跳到主要内容

Kubernetes 部署 HTTP 服务

编写一个简单的服务

生成一些笑话

package main

import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"time"
)

type Joke struct {
Setup string `json:"setup"`
Punchline string `json:"punchline"`
}

func main() {
rand.Seed(time.Now().UnixNano())

jokes := []Joke{
{Setup: "为什么老虎不喜欢玩扑克牌?", Punchline: "因为他们总是被抓牌!"},
{Setup: "为什么小学生喜欢上学?", Punchline: "因为他们离开了家庭作业!"},
{Setup: "为什么会有“狗不理包子”?", Punchline: "因为狗不会做包子啊!"},
{Setup: "一个数学家和一只普通人有什么不同?", Punchline: "数学家有解!"},
{Setup: "为什么小鸟飞不高?", Punchline: "因为它的体重不够大!"},
}

http.HandleFunc("/joke", func(w http.ResponseWriter, r *http.Request) {
joke := jokes[rand.Intn(len(jokes))]

jsonData, err := json.Marshal(joke)
if err != nil {
http.Error(w, "Unable to marshal JSON", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(jsonData)
})

fmt.Println("Starting server on port 8080...")
http.ListenAndServe(":8080", nil)
}

访问上面的服务

curl http://localhost:8080/joke

打包成容器镜像

创建一个 Dockerfile 文件,指定容器的基础镜像、应用程序代码的拷贝方式、容器启动命令等信息。例如:

# 基础镜像
FROM golang:1.17

# 设置工作目录
WORKDIR /app

# 拷贝应用程序代码
COPY . .

# 构建应用程序
RUN go build -o jokes main.go

# 启动命令
CMD ["./jokes"]

执行 docker build 命令来构建镜像,例如:

# 这里使用私有库
docker build -t localhost:5000/jokes:latest .
# 推送到私有库
docker push localhost:5000/jokes:latest

使用以下命令查看本地 Docker 镜像仓库中的镜像:

curl -s http://localhost:5000/v2/_catalog | jq .

执行上述命令后,将会得到一个 JSON 格式的输出,其中 repositories 键对应的值即为私有仓库中包含的镜像列表。如果私有仓库为空,则该值将为空数组。可以进一步执行如下命令,查看某个镜像的标签列表:

curl -s http://localhost:5000/v2/<IMAGE_NAME>/tags/list | jq .

其中,<IMAGE_NAME> 需要替换为你需要查询标签的镜像名称。执行上述命令后,将会得到一个 JSON 格式的输出,其中 tags 键对应的值即为该镜像对应的标签列表。

部署到 Kubernetes 里面

在项目根目录下创建两个 YAML 配置文件(这里使用私有的 Docker 仓库):

deployment.yaml 文件内容如下:

apiVersion: apps/v1
kind: Deployment # 定义了 Kubernetes 部署的类型
metadata:
name: jokes-deployment
labels:
app: jokes
spec:
replicas: 1 # 定义了要启动的 Pod 的数量
selector: # 定义了要选择哪些 Pod 进行部署
matchLabels:
app: jokes
template: # 定义了要部署的 Pod 的模板
metadata:
labels:
app: jokes
spec:
containers: # 定义了要运行的容器
- name: jokes
image: localhost:5000/jokes:latest
ports:
- containerPort: 8080

service.yaml 文件内容如下:

apiVersion: v1
kind: Service
metadata:
name: jokes-service
labels:
app: jokes
spec:
selector:
app: jokes
ports:
- name: http
port: 21980 # 定义了服务暴露的端口
targetPort: 8080
type: LoadBalancer
提示

在 Kubernetes 中,Service 的 port 和 targetPort 是用于指定 Service 监听的端口和后端 Pod 上暴露的端口,它们的区别如下:

  • port:Service 暴露的端口,即客户端可以使用的端口。当客户端通过 Service 访问后端 Pod 时,需要使用的端口就是 port 指定的端口。如果未指定 port,则默认使用 Service 的名称作为端口号。
  • targetPort:后端 Pod 暴露的端口,即 Service 负载均衡流量时使用的端口。如果 Service 负载均衡到的后端 Pod 上没有暴露 targetPort 指定的端口,则请求将无法正确路由到后端 Pod 上。

举个例子,假设您有一个后端 Pod,该 Pod 在容器内部运行一个 Web 服务器,监听端口 8080。您还创建了一个 Service,该 Service 将请求路由到该 Pod 上。

为了让 Service 能够正确地路由请求,您需要将 Service 的 targetPort 设置为 8080,以指定应将请求路由到 Pod 上的 8080 端口。而客户端需要使用的端口可以设置为任何您希望的端口,例如 80、443 等,这些都可以在 Service 的 port 属性中指定。

需要注意的是,当您使用 NodePort 或 LoadBalancer 类型的 Service 时,还可以使用 nodePort 属性将 Service 公开到主机上的端口。nodePort 是 NodePort 类型 Service 使用的端口,它将流量路由到 Service 的 port 上,并且这个 nodePort 端口将被公开到集群节点的外部。在这种情况下,nodePort 和 port 是不同的端口。(不指定则随机生成)

使用以下命令部署应用程序:

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

这将会启动一个包含一个副本的 Deployment 和一个 LoadBalancer 类型

使用以下命令来查看部署的状态:

kubectl get deployments
kubectl get services

20230505182446

提示

后面的这个 30462 是 Kubernetes Service 的节点端口(node port)。当创建一个 Kubernetes Service 时,它将创建一个虚拟 IP 地址,该 IP 地址将指向集群中运行的所有 Pod 的一个或多个实例。为了访问该服务,需要公开此虚拟 IP 地址。

Kubernetes Service 可以通过多种方式公开,其中一种方式是将其公开为节点端口。节点端口是在 Kubernetes 集群中的每个节点上公开服务的端口号。

在创建 Kubernetes Service 时,可以选择将服务公开为节点端口。在上面的示例中,服务名称为 jokes-service,类型为 LoadBalancer,并且在节点端口 30462 上公开端口 21980,这意味着在节点上访问 jokes-service 服务的端口 30462 时,将使用负载平衡器将流量路由到服务的端口 21980。

可以直接进入启动的服务的容器,去检查一下服务是否启动了

# kubectl exec -it <pod-name> -- <command>
kubectl exec -it jokes-deployment-67b489cfc5-h444z -- /bin/bash

20230505183023

根据上面暴露的 IP 去访问服务(暴露的其实就是本地 IP,所以这里直接使用 localhost),可以看到服务的返回值了

20230505192320

检查服务状态

20230505175746

如果 LoadBalancer 类型的 Service 的 EXTERNAL-IP 列显示为 <pending>,说明该 Service 暂时没有分配到外部 IP。

要查看 LoadBalancer 的分配状态,可以执行以下命令:

kubectl describe services <service-name>

该命令会显示 Service 的详细信息,包括当前的状态、事件等。

20230505181137

如果没有啥异常,可以检查一下是不是 Port 被占用了,修改后就可以访问了

20230505182446

k8s 辨析 port、NodePort、targetPort、containerPort 区别

NodePort

nodePort 提供了集群外部客户端访问 Service 的一种方式,nodePort 提供了集群外部客户端访问 Service 的端口,通过 nodeIP:nodePort 提供了外部流量访问 k8s 集群中 service 的入口。

比如外部用户要访问k8s集群中的一个Web应用,那么我们可以配置对应 service 的 type=NodePort,nodePort=30001。其他用户就可以通过浏览器 http://node:30001 访问到该web服务。

而数据库等服务可能不需要被外界访问,只需被内部服务访问即可,那么我们就不必设置service的NodePort。

而数据库等服务可能不需要被外界访问,只需被内部服务访问即可,那么我们就不必设置 service 的 NodePort。

port

port 是暴露在 clusterIP 上的端口,port 提供了集群内部客户端访问 service 的入口,即 clusterIP:port

mysql 容器暴露了 3306 端口(参考 DockerFile),集群内其他容器通过 33306 端口访问 mysql 服务,但是外部流量不能访问 mysql 服务,因为 mysql 服务没有配置 NodePort。对应的 service.yaml 如下:

apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
ports:
- port: 33306
targetPort: 3306
selector:
name: mysql-pod

targetPort

targetPort 是 pod 上的端口,从 port/nodePort 上来的数据,经过 kube-proxy 流入到后端 pod 的 targetPort 上,最后进入容器。

与制作容器时暴露的端口一致(使用 DockerFile 中的 EXPOSE),例如官方的 nginx(参考DockerFile)暴露80端口。 对应的 service.yaml 如下:

apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort # 配置NodePort,外部流量可访问k8s中的服务
ports:
- port: 30080 # 服务访问端口,集群内部访问的端口
targetPort: 80 # pod控制器中定义的端口(应用访问的端口)
nodePort: 30001 # NodePort,外部客户端访问的端口
selector:
name: nginx-pod

containerPort

containerPort是在pod控制器中定义的、pod中的容器需要暴露的端口。

例如,mysql 服务需要暴露 3306 端口,redis 暴露 6379 端口

apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master
labels:
name: redis-master
spec:
replicas: 1
selector:
name: redis-master
template:
metadata:
labels:
name: redis-master
spec:
containers:
- name: master
image: kubeguide/redis-master
ports:
- containerPort: 6379 # 此处定义暴露的端口

检查服务日志

如果上面的服务一直没有启来,可以通过以下方式去检查为啥服务没有起来

1、使用 kubectl describe deployment 命令查看 Deployment 的详细信息,包括 Pod 的创建状态、事件和各个容器的状态:

kubectl describe deployment <deployment-name>

20230505150917

2、使用 kubectl get pods 命令查看 Pod 的状态,包括 Pod 是否已经创建和正在运行的容器:

kubectl get pods

如果 Pod 处于 Pending 状态,可能是因为调度器无法找到合适的节点来运行 Pod。可以使用 kubectl describe pod 命令查看 Pod 的详细信息,包括 Pod 调度的状态和调度器的事件:

kubectl describe pod <pod-name>

20230505151307

3、使用 kubectl logs 命令查看容器的日志,以查找容器启动失败的原因:

kubectl logs <pod-name> <container-name>

如果容器未能启动,可能是由于容器映像中缺少必要的文件或配置,或者容器启动时发生错误。在容器日志中查找错误消息和异常堆栈跟踪,可以帮助你确定容器启动失败的原因。

4、最后,可以查看 Kubernetes 控制面板的事件日志,以查找与 Deployment 相关的事件和错误:

kubectl get events

这些事件可能包括 Pod 调度失败、容器启动失败等等。通过查看事件日志,可以快速定位 Deployment 未启动的原因。

更新服务

在 Kubernetes 中,更新 Deployment 的常见做法是通过执行 kubectl apply 命令来部署更新。这个命令会自动检测当前的状态,并尝试更新它。如果 Deployment 对象不存在,它会被创建;如果存在,那么 Deployment 的更新策略会根据更新时的参数进行更新,比如滚动更新等。

如果已经修改了 Deployment 的配置,可以通过以下命令重新加载:

kubectl apply -f deployment.yaml

这会将 deployment.yaml 文件中的配置应用到当前的 Deployment 对象中,并尝试更新它。如果存在任何问题,kubectl apply 命令将输出错误信息。否则,Deployment 将开始滚动更新,直到新的 Pod 代替旧的 Pod。

References